function [filesfound, numberfound] = findfiles(varargin)
% findfiles  - Linux/UNIX-like find command/function
%
% returns a list in which all files are listed that match one of the
% patterns.
%
% FORMAT:
% [files, number] = findfiles(startfolder, patterns, opts, ....)
%
% - startfolder     <char array> folder where to start search
% - patterns        <cell array <char array>> file patterns (shell-like)
%   (alt.)          <char array>  : single pattern
% - opt             <struct array>: optional parameters in the form of
%   .cellstr        <double array>: if set and !0 -> return as cellstr
%   .chararr        <double array>: if set and !0 -> return as char array
%   .depth          <double array>: sets both minimum and maximum depth
%   .dirs           <double array>: if set and !0 -> dirs instead of files
%   .filesize       <double array>: if set and !0 -> only matching files
%   .maxdepth       <double array>: maximum depth where to search
%   .mindepth       <double array>: minimum depth where to search
%   .maxage         <double array>: seconds file must have changed in
%   .minage         <double array>: seconds file must not have changed in
%   .oneperdir      <double array>: if set and !0 -> only first match
%   .relative       <... array>   : if set -> !0 -> './' // char array
%
% when used as a command, multiple opts can be given as multiple arguments,
% seperated by spaces (' '):
%
% findfiles /search/folder *mypattern*.txt depth=3 oneperdir=1 relative=./
%
% when used in functional context, a second return value, the number
% of matching files, can be obtained:
%
% [files,number] = findfiles('/where','*.txt');
%
% NOTE: the minage/maxage feature only fully works when the system
% returns English-style month in calls to dir. i.e. under Linux, set the
% LANG environmental setting to 'en_US' before starting up MatLab

% Version:  v0.7f
% Build:    8110521
% Date:     Nov-05 2008, 9:00 PM CET
% Author:   Jochen Weber, SCAN Unit, Columbia University, NYC, NY, USA
% URL/Info: http://wiki.brainvoyager.com/BVQXtools

% enough arguments ?
if nargin < 1 || ...
   ((~ischar(varargin{1}) || ...
     isempty(varargin{1})) && ...
    (~iscell(varargin{1}) || ...
     isempty(varargin{1}) || ...
     ~ischar(varargin{1}{1}) || ...
     isempty(varargin{1}{1})))
    error( ...
        'BVQXtools:TooFewArguments',...
        'Too few arguments. Try ''help %s''.',...
        mfilename ...
    );
end
if nargin == 1
    [p, f, x] = fileparts(varargin{1}(:)');
    if isempty(p) || ...
        strcmp(p, '.')
        varargin{1} = pwd;
    else
        varargin{1} = p;
    end
    varargin{2} = [f, x];
end

% shortcut variable
fsep = filesep;

% sanity checks: startfolder
startfolder = varargin{1};
if ischar(startfolder) && ...
   ~isempty(startfolder)

    % does startfolder contain an asterisk?
    if any(startfolder == '?' | startfolder == '*')

        % then put startfolder into a cell array to treat this case
        [filesfound, numberfound] = findfiles({startfolder}, varargin{2:end});
        return;
    end

% startfolder is a list of folders
elseif iscell(startfolder) && ...
   ~isempty(startfolder)

    % generate expanded list
    nstartfolder = cell(0);

    % iterate over items
    for nelem = 1:numel(startfolder)

        % must be non-empty char
        if ~ischar(startfolder{nelem}) || ...
            isempty(startfolder{nelem})
            error( ...
                'BVQXtools:BadArgument',...
                'Bad startfolder argument.' ...
            );
        end

        % expand pattern?
        if ~any(startfolder{nelem} == '?' | startfolder{nelem} == '*')

            % put into final list
            nstartfolder{end+1} = startfolder{nelem};

        % or look up matching folders
        else

            % split along possible path separators
            [pparts, cparts] = splittocell(startfolder{nelem}, '/\', 1, 1);
            if any('/\:' == startfolder{nelem}(1))
                pparts = [{''}, pparts];
                cparts = cparts + 1;
            end

            % look for first pattern
            for cpart = 1:cparts
                if any(pparts{cpart} == '?' | pparts{cpart} == '*')
                    break;
                end
            end

            % if the pattern occurs in first part, look in current dir
            if cpart==1
                [pfolders, npfolders] = findfiles('.', ...
                    pparts{1}, ...
                    struct('dirs', 1, 'depth', 1));

            % otherwise glue first parts together and look up matches
            else
                [pfolders, npfolders] = findfiles( ...
                    gluetostring({pparts{1:(cpart-1)}},fsep), ...
                    pparts{cpart}, ...
                    struct('dirs', 1, 'depth', 1));
            end

            % the pattern was not in last part
            if cpart < cparts

                % put remaining parts back on
                for ppart = 1:npfolders
                    pfolders{ppart} = [pfolders{ppart} fsep ...
                        gluetostring({pparts{(cpart+1):end}},fsep)];
                end
            end

            % put results at end of list
            nstartfolder = [nstartfolder, pfolders];
        end
    end

    % we start with no files found
    filesfound = cell(0);

    % for each folder in list
    for nelem = 1:numel(nstartfolder)

        % if there (iteratively) remains a pattern char, redo this
        if any(nstartfolder{nelem} == '?' | nstartfolder{nelem} == '*')
            filesfound = [filesfound(1:end); ...
                findfiles({nstartfolder{nelem}}, varargin{2:end})];

        % otherwise get files in this folder and put at end of array
        elseif exist(nstartfolder{nelem}, 'dir') == 7
            filesfound = [filesfound(1:end); ...
                findfiles(nstartfolder{nelem}, varargin{2:end})];
        end
    end

    % report the total number found
    numberfound = numel(filesfound);
    return;

% illegal first argument
else
    error( ...
        'BVQXtools:BadArgument',...
        'Bad startfolder argument.'...
    );
end

% we're now going for the single folder case and startfolder is OK

% append missing fsep if needed
if startfolder(end) ~= fsep
    startfolder = [startfolder fsep];
end

% startfolder exists?
if exist(startfolder,'dir') ~= 7
    error( ...
        'BVQXtools:FolderNotFound',...
        'Startfolder (%s) not found or no folder.',...
        strrep(startfolder,'\','\\') ...
    );
end

% - sanity checks patterns
patterns = varargin{2};

% default is cell, otherwise
if ~iscell(patterns)

    % if is character, put into cell
    if ischar(patterns)
        patterns = { patterns };

    % otherwise bail out
    else
        error( ...
            'BVQXtools:BadArgument', ...
            'Patterns must either be a single string or a cell array!' ...
        );
    end
end

% check each pattern
for count = 1:numel(patterns)

    % only accept chars
    if ~ischar(patterns{count}) || ...
        isempty(patterns{count})
        patterns{count} = '*';
    end
end
patterns = unique(patterns(:));

% option argument parsing, default options
if nargin < 3
    opt.dirs = 0;
    opt.filesize = 0;
    opt.maxdepth = 0;
    opt.mindepth = 0;
    opt.maxage = -1;
    opt.minage = -1;
    opt.oneperdir = 0;
    opt.relative = 0;
    opt.return = 'cellarr';
    opt.rfolder = startfolder;

% parse options
else
    opt = varargin{3};

    % non-struct options
    if ~isstruct(opt)

        % yet start with default struct
        clear opt;
        opt.dirs = 0;
        opt.filesize = 0;
        opt.maxdepth = 0;
        opt.mindepth = 0;
        opt.maxage = -1;
        opt.minage = -1;
        opt.oneperdir = 0;
        opt.relative = 0;
        opt.return = 'cellarr';
        opt.rfolder = startfolder;

        % parse all arguments
        for acount=3:nargin

            % char option
            if ischar(varargin{acount})

                % get argument name
                argnv = splittocell(varargin{acount}, '=', 1);
                oname = argnv{1};

                % and possible option value
                if size(argnv,2) > 1
                    oval = argnv{2};
                else
                    oval = '';
                end

                % only accept known arguments
                switch lower(oname)

                    % option: cellstr, set return type
                    case {'cellstr'}
                        oval = str2double(oval);
                        if oval ~= 0
                            opt.return = 'cellstr';
                        end

                    % option: chararr, set return type
                    case {'chararr'}
                        oval = str2double(oval);
                        if oval ~= 0
                            opt.return = 'chararr';
                        end

                    % option: depth (min and max)
                    case {'depth'}
                        if str2double(oval) >= 0
                            opt.maxdepth = str2double(oval);
                            opt.mindepth = str2double(oval);
                        else
                            opt.maxdepth=0;
                            opt.mindepth=0;
                        end

                    % option: dirs, set lookup type
                    case {'dirs'}
                        oval = str2double(oval);
                        if oval == 0
                            opt.dirs = 0;
                        else
                            opt.dirs = 1;
                        end

                    % option: filesize
                    case {'filesize'}
                        if str2double(oval) >= 0
                            opt.filesize = fix(str2double(oval));
                        else
                            opt.filesize = 0;
                        end

                    % option: maxdepth
                    case {'maxdepth'}
                        if str2double(oval) >= 0
                            opt.maxdepth = fix(str2double(oval));
                        else
                            opt.maxdepth = 0;
                        end

                    % option: mindepth
                    case {'mindepth'}
                        if str2double(oval) >= 0
                            opt.mindepth = fix(str2double(oval));
                        else
                            opt.mindepth = 0;
                        end

                    % option: maxage
                    case {'maxage'}
                        if str2double(oval) >= 0
                            opt.maxage = fix(str2double(oval));
                        else
                            opt.maxage = -1;
                        end

                    % option: minage
                    case {'minage'}
                        if str2double(oval) >= 0
                            opt.minage = fix(str2double(oval));
                        else
                            opt.minage = -1;
                        end

                    % option: oneperdir
                    case {'oneperdir'}
                        oval = str2double(oval);
                        if oval == 0
                            opt.oneperdir = 0;
                        else
                            opt.oneperdir = 1;
                        end

                    % option: relative
                    case 'relative'
                        noval = str2double(oval);
                        if ~isnan(noval)
                            opt.relative = noval;
                            if noval < 1
                                opt.rfolder = startfolder;
                            else
                                opt.rfolder = ['.' fsep];
                            end
                        else
                            opt.relative = 1;
                            opt.rfolder = oval;
                        end;
                end
            end
        end

    % struct option argument
    else

        % make sure options are present
        if ~isfield(opt, 'dirs')
            opt.dirs = 0;
        end
        if ~isfield(opt, 'filesize')
            opt.filesize = 0;
        end
        if ~isfield(opt, 'maxdepth')
            if isfield(opt, 'depth')
                opt.maxdepth = opt.depth;
            else
                opt.maxdepth = 0;
            end
        end
        if ~isfield(opt, 'mindepth')
            if isfield(opt, 'depth')
                opt.mindepth = opt.depth;
            else
                opt.mindepth = 0;
            end
        end
        if ~isfield(opt, 'maxage')
            opt.maxage = -1;
        end
        if ~isfield(opt, 'minage')
            opt.minage = -1;
        end
        if ~isfield(opt, 'oneperdir')
            opt.oneperdir = 0;
        end
        if  isfield(opt, 'rfolder')
            opt = rmfield(opt, 'rfolder');
        end
        if ~isfield(opt, 'relative')
            opt.relative = 0;
            opt.rfolder = startfolder;
        else
            if ischar(opt.relative)
                opt.rfolder=opt.relative;
                opt.relative=1;
            else
                if double(opt.relative) >= 1
                    opt.rfolder = ['.' fsep];
                    opt.relative = 1;
                else
                    opt.rfolder = startfolder;
                    opt.relative = 0;
                end
            end
        end
        if ~isfield(opt, 'return')
            opt.return = 'cellarr';
        end
    end
end

% more interdependent checks now
if isfield(opt,'cellstr') && ...
    opt.cellstr > 0
    opt.return = 'cellstr';
end
if isfield(opt,'chararr') && ...
    opt.chararr > 0
    opt.return = 'chararr';
end
if opt.dirs ~= 0
    opt.dirs = 1;
end

% check option types
if ~isa(opt.filesize, 'double')
    opt.filesize = 0;
end
if ~isa(opt.maxdepth, 'double')
    opt.maxdepth = 0;
end
if ~isa(opt.mindepth, 'double')
    opt.mindepth = 0;
end
if ~isa(opt.maxage, 'double')
    opt.maxage = -1;
end
if ~isa(opt.minage, 'double')
    opt.minage = -1;
end
if opt.oneperdir ~= 0
    opt.oneperdir = 1;
end
if opt.relative ~= 0
    opt.relative = 1;
else opt.rfolder = startfolder;
end

% calculate age here
opt.maxage=opt.maxage / 86400;
if opt.maxage < 0
    opt.maxage = -1;
end
opt.minage = opt.minage / 86400;
if opt.minage < 0
    opt.minage = -1;
end

% make call for files
if opt.dirs == 0
    filesfound = findsubfiles( ...
        startfolder, patterns, 1, ...
        opt.filesize, opt.mindepth, opt.maxdepth, ...
        opt.minage, opt.maxage, ...
        opt.oneperdir, opt.rfolder);

% make call for dirs
else
    filesfound = findsubdirs( ...
        startfolder, patterns, 1, ...
        opt.mindepth, opt.maxdepth, ...
        opt.minage, opt.maxage, ...
        opt.oneperdir, opt.rfolder);
end

% return the correct number of values
if nargout > 1
    numberfound = size(filesfound, 1);
end

% return correct type
if strcmpi(opt.return(:)', 'chararr')
	filesfound = char(filesfound);
end

% - end of findfiles(...)


% %%%%internal functions%%%%


% findsubfiles
function found = findsubfiles(path, patterns, adepth, fsize, sdepth, mdepth, mnage, mxage, operdir, relative)

% start with zero files found
nfound = 0;
found = { };
mfilesep = filesep;

% first, recursively handle all subfolders, if depth is still valid
if mdepth == 0 || ...
    adepth < mdepth

    % get list of files and folders, and size of list
    ilist = dir(path);
    slist = size(ilist, 1);

    % get isdir flag into array and find indices of dirs
    [ilistd(1:slist)] = [ilist(:).isdir];
    ilistd = find(ilistd > 0);

    % check items
    for count = ilistd

        % don't heed . and ..
        if strcmp(ilist(count).name, '.') || ...
            strcmp(ilist(count).name, '..')
            continue;
        end;

        % find files in subdirs
        filestoadd = findsubfiles([path ilist(count).name mfilesep], ...
            patterns, adepth + 1, fsize, sdepth, mdepth, ...
            mnage, mxage, operdir, [relative ilist(count).name mfilesep]);
        sfound = size(filestoadd, 1);

        % if files found
        if sfound > 0
            nfoundfrm = nfound + 1;
            nfoundnew = nfound + sfound;
            found(nfoundfrm:nfoundnew) = filestoadd(:);
            nfound = nfoundnew;
        end
    end
end

% then, if depth is valid, add files to the output
if sdepth == 0 || ...
    sdepth <= adepth

    % only get time if needed
    if any([mnage, mxage] >= 0)
        rnow = now;
    end;

    % number of patterns
    spatt = size(patterns, 1);
    for pcount = 1:spatt
        
        % no "*" pattern
        if ~any(patterns{pcount} == '*')
            ilist = dir([path patterns{pcount} '*']);
            if isempty(ilist)
                continue;
            end
            ilistn = {ilist(:).name};
            if any(strcmp(ilistn, patterns{pcount}))
                nfound = nfound + 1;
                found{nfound} = [relative patterns{pcount}];
            end
            continue;
        end

        % find matching entries
        ilist = dir([path patterns{pcount}]);
        slist = numel(ilist);

        % get isdir flag into array and remove dirs from list
        ilistd = [];
        [ilistd(1:slist)] = [ilist(:).isdir];
        ilist(ilistd > 0) = [];
        slist = numel(ilist);

        % if only one per dir
        if operdir == 1
            count = 1;

            % reject all non-matching
            while count <= slist && ...
                 ((mnage >= 0 && (rnow - datenum(ilist(count).date)) < mnage) || ...
                  (mxage >= 0 && (rnow - datenum(ilist(count).date)) > mxage) || ...
                  (fsize ~= 0 && ilist(count).bytes ~= fsize))
                count = count + 1;
            end

            % choose first if still valid
            if count <= slist
                nfound = nfound + 1;
                found{nfound} = [relative ilist(count).name];
            end

        % otherwise check all
        else

            % iterate over all
            for count = 1:slist

                % reject non-matching
                if ((mnage >= 0 && (rnow - datenum(ilist(count).date)) < mnage) || ...
                    (mxage >= 0 && (rnow - datenum(ilist(count).date)) > mxage) || ...
                    (fsize ~= 0 && ilist(count).bytes ~= fsize))
                    continue;
                end

                % accept rest
                nfound = nfound + 1;
                found{nfound} = [relative ilist(count).name];
            end
        end
    end

    % linearize found
    found = found(:);
end
% end of function findsubfiles

% findsubdirs
function found = findsubdirs(path, patterns, adepth, sdepth, mdepth, mnage, mxage, operdir, relative)

% start with zero dirs found
nfound = 0;
found = { };
mfilesep = filesep;

% first, recursively handle all subfolders, if depth is still valid
if mdepth == 0 || ...
    adepth < mdepth

    % get list of files and folders, and size of list
    ilist = dir(path);
    slist = size(ilist, 1);

    % get isdir flag into array
    [ilistd(1:slist)] = [ilist(:).isdir];

    % find indices of dirs
    ilistd = find(ilistd > 0);

    % check items
    for count = ilistd

        % don't heed . and ..
        if strcmp(ilist(count).name, '.') || ...
            strcmp(ilist(count).name, '..')
            continue;
        end

        % iterate over subdirs
        filestoadd = findsubdirs([path ilist(count).name mfilesep], ...
            patterns, adepth + 1, sdepth, mdepth, ...
            mnage, mxage, operdir, [relative ilist(count).name mfilesep]);
        sfound = size(filestoadd, 1);

        % if dirs founds
        if sfound > 0
            nfoundfrm = nfound + 1;
            nfoundnew = nfound + sfound;
            found(nfoundfrm:nfoundnew) = filestoadd(:);
            nfound = nfoundnew;
        end
    end
end

% then, if depth is valid, add folders to the output
if sdepth == 0 || ...
    sdepth <= adepth

    % only get time if needed
    if any([mnage, mxage]>=0)
        rnow = now;
    end;

    % number of patterns
    spatt = size(patterns, 1);
    for pcount = 1:spatt

        % no "*" pattern
        if ~any(patterns{pcount} == '*')
            if exist([path patterns{pcount}], 'dir') == 7
                nfound = nfound + 1;
                found{nfound} = [relative patterns{pcount}];
            end
            continue;
        end

        % get matching entries
        ilist = dir([path patterns{pcount}]);
        slist = numel(ilist);

        % get isdir flag into array and remove files from list
        ilistd = [];
        [ilistd(1:slist)] = [ilist(:).isdir];
        ilist(~ilistd) = [];
        slist = numel(ilist);

        % if only one per dir
        if operdir == 1
            count = 1;

            % reject all non-matching entries
            while count <= slist && ...
                 ((mnage >= 0 && (rnow - datenum(ilist(count).date)) < mnage) || ...
                  (mxage >= 0 && (rnow - datenum(ilist(count).date)) > mxage))
                count = count + 1;
            end

            % find next entry
            while count <= slist

                % still reject . and ..
                if  strcmp(ilist(count).name, '.') || ...
                    strcmp(ilist(count).name, '..')
                    count = count + 1;
                    continue;
                end

                % get next entry
                nfound = nfound + 1;
                found{nfound} = [relative ilist(count).name];
                break;
            end

        % otherwise check all
        else

            % iterate over all
            for count = 1:slist

                % reject non-matching
                if ((mnage >= 0 && (rnow - datenum(ilist(count).date)) < mnage) || ...
                    (mxage >= 0 && (rnow - datenum(ilist(count).date)) > mxage))
                    continue;
                end

                % reject . and ..
                if strcmp(ilist(count).name, '.') || ...
                    strcmp(ilist(count).name, '..')
                    continue;
                end

                % accept others
                nfound = nfound + 1;
                found{nfound} = [relative ilist(count).name];
            end
        end
    end

    % linearize found
    found = found(:);
end
% end of function findsubdirs
